#----------------------------------------------
# -*- encoding=utf-8 -*-                      #
# __author__:'xiaojie'                        #
# CreateTime:                                 #
#       2019/4/23 14:34                       #
#                                             #
#               天下风云出我辈，                 #
#               一入江湖岁月催。                 #
#               皇图霸业谈笑中，                 #
#               不胜人生一场醉。                 #
#----------------------------------------------

#infoGAN的keras实现
# 参考代码:https://github.com/eriklindernoren/Keras-GAN/blob/master/infogan/infogan.py
# 总结，在keras中，输入对输出是一个模型，有几个输出，就有几个model，比如对discriminatory而已，输入真实
# 的X，即优化真实的X对应的交叉熵，输入生成的X，即优化生成的X对应交叉熵。对generator而已，输入的是Z，要经过
# discriminatory来优化，但输出有2个，

from __future__ import print_function, division

from keras.datasets import mnist
from keras.layers import Input, Dense, Reshape, Flatten, Dropout, multiply, concatenate
from keras.layers import BatchNormalization, Activation, Embedding, ZeroPadding2D, Lambda
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import UpSampling2D, Conv2D
from keras.models import Sequential, Model
from keras.optimizers import Adam
from keras.utils import to_categorical
from keras.utils import plot_model
import keras.backend as K

import matplotlib.pyplot as plt

import numpy as np

class INFOGAN():
    def __init__(self):
        self.img_rows = 28
        self.img_cols = 28
        self.channels = 1
        self.num_classes = 10
        self.img_shape = (self.img_rows, self.img_cols, self.channels)

        self.latent_dim = 72

        optimizer = Adam(0.0002, 0.5)
        losses = ['binary_crossentropy', self.mutual_info_loss]

        # Build the discriminator and recognition network
        self.discriminator,self.auxiliary = self.build_disk_and_q_net()

        self.discriminator.compile(loss=['binary_crossentropy'],
                                   optimizer=optimizer,
                                   metrics=['accuracy'])

        # Build and recognition network Q
        self.auxiliary.compile(loss=[self.mutual_info_loss],
                               optimizer=optimizer,
                               metrics=['accuracy'])

        # Build the generator
        self.generator = self.build_generator()

        # The generator takes noise and the target label as input
        # and generates the corresponding digit of that label
        gen_input = Input(shape=(self.latent_dim,))
        img = self.generator(gen_input)
        print('AAAAAAAAAAAA',img.shape)

        # For the combined model we will only train the generator
        self.discriminator.trainable = False

        #  The discriminator takes generated image as input and determines validity
        valid =self.discriminator(img)
        # The recognition network produces the label
        target_label = self.auxiliary(img)

        # The combined model (stacked generator and discriminator)
        self.combined  = Model(gen_input,[valid,target_label],name='combined')

        self.combined.compile(loss=losses,optimizer=optimizer)
        plot_model(self.combined, to_file='png/combined_model.png', show_shapes=True)

    def build_generator(self):
        model = Sequential(name='generator')

        model.add(Dense(128 * 7 * 7, activation="relu", input_dim=self.latent_dim))
        model.add(Reshape((7, 7, 128)))
        model.add(BatchNormalization(momentum=0.8))
        model.add(UpSampling2D())
        model.add(Conv2D(128, kernel_size=3, padding="same"))
        model.add(Activation("relu"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(UpSampling2D())
        model.add(Conv2D(64, kernel_size=3, padding="same"))
        model.add(Activation("relu"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Conv2D(self.channels, kernel_size=3, padding='same'))

        model.add(Activation("tanh"))

        gen_input = Input(shape=(self.latent_dim,))
        img = model(gen_input)

        generator_model = Model(gen_input,img,name='generator_model')
        plot_model(generator_model,to_file='png/gererator_model.png',show_shapes=True)

        return generator_model

    def build_disk_and_q_net(self):
        img = Input(shape=self.img_shape)

        # Shared layers between discriminator and recognition network
        model = Sequential(name='CNN')
        model.add(Conv2D(64, kernel_size=3, strides=2, input_shape=self.img_shape, padding="same"))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.25))
        model.add(Conv2D(128, kernel_size=3, strides=2, padding="same"))
        model.add(ZeroPadding2D(padding=((0, 1), (0, 1))))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.25))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Conv2D(256, kernel_size=3, strides=2, padding="same"))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.25))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Conv2D(512, kernel_size=3, strides=2, padding="same"))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.25))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Flatten())

        img_embedding = model(img)
        print('BBBBBBBBBBBB',img_embedding.shape)

        # Discriminator
        validity = Dense(1,activation='sigmoid')(img_embedding)

        # Recognition
        q_net = Dense(128,activation='relu')(img_embedding)
        label = Dense(self.num_classes,activation='softmax')(q_net)

        # Return discriminator and recognition network
        model1 = Model(img,validity,name='discriminator')
        model2 = Model(img,label,name='classifier')
        plot_model(model1,to_file='png/discriminator.png',show_shapes=True)
        plot_model(model2, to_file='png/classifier.png', show_shapes=True)

        return model1,model2

    def mutual_info_loss(self,c,c_give_x):
        """The mutual information metric we aim to minimize"""
        eps = 1e-8
        conditional_entropy = K.mean(-K.sum(K.log(c_give_x+eps)*c,axis=1))
        entropy =             K.mean(-K.sum(K.log(c+eps)*c,axis=1))
        return conditional_entropy+entropy

    def sample_generator_input(self,batch_size):
        #Generator inputs
        sampled_noise = np.random.normal(0,1,(batch_size,62))
        sampled_labels = np.random.randint(0,self.num_classes,batch_size).reshape(-1,1)
        sampled_labels = to_categorical(sampled_labels,num_classes=self.num_classes)
        return sampled_noise,sampled_labels

    def train(self,epochs,batch_size=128,sample_interval=50):
        # Load the dataset
        (X_train,y_train),(_,_) = mnist.load_data()

        # Rescale -1 to 1
        X_train = (X_train.astype(np.float32)-127.5)/127.5
        X_train = np.expand_dims(X_train,axis=3)
        y_train = y_train.reshape(-1,1)

        # Adversarial ground truths
        valid = np.ones((batch_size,1))
        fake  = np.zeros((batch_size,1))

        for epoch in range(epochs):
            # ---------------------
            #  Train Discriminator
            # ---------------------

            # Select a random half batch of images
            idx = np.random.randint(0,X_train.shape[0],batch_size)
            imgs = X_train[idx]

            # Sample noise and categorical labels
            sampled_noise,sampled_labels = self.sample_generator_input(batch_size)
            gen_input = np.concatenate((sampled_noise,sampled_labels),axis=1)

            # Generate a half batch of images
            gen_imgs = self.generator.predict(gen_input)

            #Train on real and generated data
            d_loss_real = self.discriminator.train_on_batch(imgs,valid)
            d_loss_fake = self.discriminator.train_on_batch(gen_imgs,fake)

            # Avg loss
            d_loss = 0.5*np.add(d_loss_real,d_loss_fake)

            # ---------------------
            #  Train Generator and Q-network
            # ---------------------

            g_loss = self.combined.train_on_batch(gen_input,[valid,sampled_labels])

            # Plot the progress
            print("%d [D loss: %.2f, acc.: %.2f%%] [Q loss: %.2f] [G loss: %.2f]" % (
                epoch, d_loss[0], 100 * d_loss[1], g_loss[1], g_loss[2]))

            # If at save interval => save generated image samples
            if epoch % sample_interval == 0:
                self.sample_images(epoch)

    def sample_images(self,epoch):
        r,c = 10,10
        fig,axs=plt.subplots(r,c)
        for i in range(c):
            sampled_noise,_ = self.sample_generator_input(c)
            label = to_categorical(np.full(fill_value=i,shape=(r,1)),num_classes=self.num_classes)
            gen_input = np.concatenate((sampled_noise,label),axis=1)
            gen_imgs = self.generator.predict(gen_input)
            gen_imgs = 0.5*gen_imgs+0.5
            for j in range(r):
                axs[j, i].imshow(gen_imgs[j, :, :, 0], cmap='gray')
                axs[j, i].axis('off')
        fig.savefig("images/%d.png" % epoch)
        plt.close()

    def save_model(self):

        def save(model, model_name):
            model_path = "saved_model/%s.json" % model_name
            weights_path = "saved_model/%s_weights.hdf5" % model_name
            options = {"file_arch": model_path,
                       "file_weight": weights_path}
            json_string = model.to_json()
            open(options['file_arch'], 'w').write(json_string)
            model.save_weights(options['file_weight'])

        save(self.generator, "generator")

        save(self.discriminator, "discriminator")

if __name__ == '__main__':
    infogan = INFOGAN()
    infogan.train(epochs=50000,batch_size=128,sample_interval=50)
